cmake_minimum_required(VERSION 3.5.1)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

function(add_kernel_module target)

# 构建目标的内核版本
# 参与构建的配置目录 /lib/modules/${KERNEL_VERSION}/build
if(KERNEL_VERSION)
    message("准备构建: ${KERNEL_VERSION} 版本的内核模块")
else()
    execute_process(
        COMMAND uname -r
        OUTPUT_VARIABLE KERNEL_VERSION
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    message("!!--> 未设置 KERNEL_VERSION, 将使用 'uname -r'")
    message("!!--> Found: linux-headers-${KERNEL_VERSION} with 'uname -r'")
endif()

set(KERNEL_HEADERS_DIR "/usr/src/linux-headers-${KERNEL_VERSION}")
set(KERNEL_BUILD_DIR "/lib/modules/${KERNEL_VERSION}/build")

# 获取目标的依赖项
set(target_deps "\$(TARGET)-objs :=                                   \\\n")
string(LENGTH ${target_deps} first_len)

foreach(item IN LISTS ARGV)
    # 目标除外 - 顺便生成目标驱动器
    if (item STREQUAL target)
        file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/${target}/Makefile
            "all:\n"
            "\t$(MAKE) -C ${KERNEL_BUILD_DIR} modules M=${CMAKE_CURRENT_BINARY_DIR}/${target} src=${CMAKE_CURRENT_SOURCE_DIR}\n"
            "clean:\n"
            "\t$(MAKE) -C ${KERNEL_BUILD_DIR} clean M=${CMAKE_CURRENT_BINARY_DIR}/${target} src=${CMAKE_CURRENT_SOURCE_DIR}\n"
        )
        # clean source directory
        add_custom_command(OUTPUT _${target}_clean
            COMMAND make clean
            WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        )
        # build
        add_custom_command(OUTPUT _${target}_build
            COMMAND make
            WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${target}
            DEPENDS _${target}_clean
            BYPRODUCTS ${target}.ko
        )
        # build all
        add_custom_command(OUTPUT _${target}_all
            COMMAND cp ${target}.ko ${CMAKE_CURRENT_SOURCE_DIR}
            WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${target}
            DEPENDS _${target}_build
        )
        add_custom_target(${target} ALL DEPENDS _${target}_all  COMMENT "制作Linux模块: ${target}.ko" )
        # set_target_properties(${target} PROPERTIES INCLUDE_DIRECTORIES "${KERNEL_INCLUDE_DIRS}")

        continue()
    endif(item STREQUAL target)

    # 计算依赖项长度，计算尾部补齐空格数，并防止小于0
    string(LENGTH ${item} itemlen)
    math(EXPR slen "${first_len}-${itemlen}-11")
    if(slen LESS_EQUAL 0)
        set(slen 1)    
    endif(slen LESS_EQUAL 0)

    # 生成空格
    set(space "")
    foreach(t RANGE 0 ${slen} 1)
        string(APPEND space " ")
    endforeach()

    # 追加到依赖条目集
    string(APPEND target_deps "        ${item}${space}\\\n")

    # 计算依赖项的目录结构，自动创建不存在的目录
    execute_process(
        COMMAND dirname ${CMAKE_CURRENT_BINARY_DIR}/${target}/${item}
        OUTPUT_VARIABLE ITEM_DIR
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )
    if (NOT EXISTS ${ITEM_DIR})
        execute_process(COMMAND mkdir ${ITEM_DIR})
    endif(NOT EXISTS ${ITEM_DIR})
endforeach(item IN LISTS ARGV)

# 将依赖项中的 .c 替换为 .o
string(REPLACE ".c" ".o" target_deps ${target_deps})

# 生成在源代码下的Makefile, 替代手写，可修改生成配置
file(WRITE ${CMAKE_CURRENT_SOURCE_DIR}/Makefile "\
# 编译目标名称设置
TARGET=${target}

obj-m += \$(TARGET).o

# 模块目标由一些被指定的目标组成
${target_deps}

# 编译开关(no support)

# 内核目录
KERNELDIR := ${KERNEL_BUILD_DIR}

all:
	\$(MAKE) -C \$(KERNELDIR) M=\$(PWD) modules
clean:
	\$(MAKE) -C \$(KERNELDIR) M=\$(PWD) clean

# 一些自主操作（不在管理范围） NOTE: 注意使用\t制表键
insmod: 
	@-sudo rmmod \$(TARGET).ko
	@sudo insmod \$(TARGET).ko
	@echo insmod 完毕

rmmod: 
	@sudo rmmod \$(TARGET).ko
	@echo rmmod 完毕

remod: 
	@-sudo insmod \$(TARGET).ko
	@-sudo rmmod \$(TARGET).ko
	@echo remod 完毕
")

endfunction(add_kernel_module)


# Usage:  注意目标名称不能与源文件名称相同

# set(KERNEL_VERSION "指定内核版本")
# include(kernel-module PATHS ${CMAKE_SOURCE_DIR})
# add_kernel_module(helloworld
#     hello.c
#     ...
# )